--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
tests/frontend/apiClientCsrfRecovery.test.js 22b4fcce096023ec706bab5449805e34a2912643 (22b4fcce) Text, 5.52 KB
T8b949e// SPDX-License-Identifier: 0BSD
Tff7b72import Tb4b4b4{ Te6edf3afterEachTb4b4b4, Te6edf3beforeEachTb4b4b4, Te6edf3describeTb4b4b4, Te6edf3expectTb4b4b4, Te6edf3itTb4b4b4, Te6edf3vi Tb4b4b4} Tff7b72from Ta5d6ff"vitest"Tb4b4b4;
Te6edf3describeTb4b4b4(Ta5d6ff"apiClient CSRF recovery"Tb4b4b4, Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4{
Te6edf3beforeEachTb4b4b4(Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4{
Te6edf3viTb4b4b4.Te6edf3resetModulesTb4b4b4(Tb4b4b4)Tb4b4b4;
Tffa657globalTb4b4b4.Tffa657window Tff7b72= Tb4b4b4{ Te6edf3locationTff7b72: Tb4b4b4{ Te6edf3originTff7b72: Ta5d6ff"http://127.0.0.1:5173" Tb4b4b4} Tb4b4b4}Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3afterEachTb4b4b4(Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4{
Te6edf3viTb4b4b4.Te6edf3unstubAllGlobalsTb4b4b4(Tb4b4b4)Tb4b4b4;
Te6edf3viTb4b4b4.Te6edf3restoreAllMocksTb4b4b4(Tb4b4b4)Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3itTb4b4b4(Ta5d6ff"retries a mutating request once after refreshing a stale CSRF token"Tb4b4b4, Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4{
T8b949e// Oracle: after a backgrounded web tab, the in-memory CSRF token can
T8b949e// disagree with the cookie session. A single 403 CSRF response must
T8b949e// refresh the token and retry, not treat the call as an auth logout.
Tff7b72const Tb4b4b4{ Te6edf3setCsrfTokenTb4b4b4, Te6edf3getCsrfToken Tb4b4b4} Tff7b72= Tff7b72await Tff7b72importTb4b4b4(Ta5d6ff"../../meshchatx/src/frontend/js/csrfToken.js"Tb4b4b4)Tb4b4b4;
Te6edf3setCsrfTokenTb4b4b4(Ta5d6ff"stale-token"Tb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3onAuthError Tff7b72= Te6edf3viTb4b4b4.Te6edf3fnTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72const Tb4b4b4{ Te6edf3createApiClient Tb4b4b4} Tff7b72= Tff7b72await Tff7b72importTb4b4b4(Ta5d6ff"../../meshchatx/src/frontend/js/apiClient.js"Tb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3api Tff7b72= Te6edf3createApiClientTb4b4b4(Tb4b4b4{ Te6edf3onAuthError Tb4b4b4}Tb4b4b4)Tb4b4b4;
Tff7b72let Te6edf3postCalls Tff7b72= T79c0ff0Tb4b4b4;
Tff7b72const Te6edf3fetchMock Tff7b72= Te6edf3viTb4b4b4.Te6edf3fnTb4b4b4(Tff7b72async Tb4b4b4(Te6edf3urlTb4b4b4, Te6edf3initTb4b4b4) Tb4b4b4=> Tb4b4b4{
Tff7b72const Te6edf3path Tff7b72= Tffa657StringTb4b4b4(Te6edf3urlTb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3pathTb4b4b4.Te6edf3includesTb4b4b4(Ta5d6ff"/api/v1/auth/csrf"Tb4b4b4) Tff7b72&& Tb4b4b4(Tff7b72!Te6edf3initTff7b72?Tb4b4b4.Te6edf3method Tff7b72|| Te6edf3initTb4b4b4.Te6edf3method Tff7b72=== Ta5d6ff"GET"Tb4b4b4)Tb4b4b4) Tb4b4b4{
Tff7b72return Tb4b4b4{
Te6edf3okTff7b72: Tff7b72trueTb4b4b4,
Te6edf3statusTff7b72: T79c0ff200Tb4b4b4,
Te6edf3headersTff7b72: Tff7b72new Te6edf3HeadersTb4b4b4(Tb4b4b4{ Ta5d6ff"content-type"Tff7b72: Ta5d6ff"application/json" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Te6edf3textTff7b72: Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tffa657JSONTb4b4b4.Te6edf3stringifyTb4b4b4(Tb4b4b4{ Te6edf3csrf_tokenTff7b72: Ta5d6ff"fresh-token" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Tb4b4b4}Tb4b4b4;
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3pathTb4b4b4.Te6edf3includesTb4b4b4(Ta5d6ff"/api/v1/destination/deadbeef/request-path"Tb4b4b4) Tff7b72&& Te6edf3initTff7b72?Tb4b4b4.Te6edf3method Tff7b72=== Ta5d6ff"POST"Tb4b4b4) Tb4b4b4{
Te6edf3postCalls Tff7b72+= T79c0ff1Tb4b4b4;
Tff7b72const Te6edf3hdrs Tff7b72= Te6edf3initTb4b4b4.Te6edf3headers Tff7b72instanceof Te6edf3Headers Tff7b72? Te6edf3initTb4b4b4.Te6edf3headers Tff7b72: Tff7b72new Te6edf3HeadersTb4b4b4(Te6edf3initTb4b4b4.Te6edf3headersTb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3token Tff7b72= Te6edf3hdrsTb4b4b4.Te6edf3getTb4b4b4(Ta5d6ff"X-CSRF-Token"Tb4b4b4)Tb4b4b4;
Tff7b72if Tb4b4b4(Te6edf3token Tff7b72=== Ta5d6ff"stale-token"Tb4b4b4) Tb4b4b4{
Tff7b72return Tb4b4b4{
Te6edf3okTff7b72: Tff7b72falseTb4b4b4,
Te6edf3statusTff7b72: T79c0ff403Tb4b4b4,
Te6edf3headersTff7b72: Tff7b72new Te6edf3HeadersTb4b4b4(Tb4b4b4{ Ta5d6ff"content-type"Tff7b72: Ta5d6ff"application/json" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Te6edf3textTff7b72: Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tffa657JSONTb4b4b4.Te6edf3stringifyTb4b4b4(Tb4b4b4{ Te6edf3errorTff7b72: Ta5d6ff"Invalid or missing CSRF token" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Tb4b4b4}Tb4b4b4;
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3token Tff7b72=== Ta5d6ff"fresh-token"Tb4b4b4) Tb4b4b4{
Tff7b72return Tb4b4b4{
Te6edf3okTff7b72: Tff7b72trueTb4b4b4,
Te6edf3statusTff7b72: T79c0ff200Tb4b4b4,
Te6edf3headersTff7b72: Tff7b72new Te6edf3HeadersTb4b4b4(Tb4b4b4{ Ta5d6ff"content-type"Tff7b72: Ta5d6ff"application/json" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Te6edf3textTff7b72: Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tffa657JSONTb4b4b4.Te6edf3stringifyTb4b4b4(Tb4b4b4{ Te6edf3messageTff7b72: Ta5d6ff"ok" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Tb4b4b4}Tb4b4b4;
Tb4b4b4}
Tb4b4b4}
Tff7b72return Tb4b4b4{
Te6edf3okTff7b72: Tff7b72falseTb4b4b4,
Te6edf3statusTff7b72: T79c0ff500Tb4b4b4,
Te6edf3headersTff7b72: Tff7b72new Te6edf3HeadersTb4b4b4(Tb4b4b4{ Ta5d6ff"content-type"Tff7b72: Ta5d6ff"application/json" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Te6edf3textTff7b72: Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tffa657JSONTb4b4b4.Te6edf3stringifyTb4b4b4(Tb4b4b4{ Te6edf3errorTff7b72: Ta5d6ff"unexpected" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Tb4b4b4}Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3viTb4b4b4.Te6edf3stubGlobalTb4b4b4(Ta5d6ff"fetch"Tb4b4b4, Te6edf3fetchMockTb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3result Tff7b72= Tff7b72await Te6edf3apiTb4b4b4.Te6edf3postTb4b4b4(Ta5d6ff"/api/v1/destination/deadbeef/request-path"Tb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3resultTb4b4b4.Te6edf3dataTb4b4b4)Tb4b4b4.Te6edf3toEqualTb4b4b4(Tb4b4b4{ Te6edf3messageTff7b72: Ta5d6ff"ok" Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3postCallsTb4b4b4)Tb4b4b4.Te6edf3toBeTb4b4b4(T79c0ff2Tb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3getCsrfTokenTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toBeTb4b4b4(Ta5d6ff"fresh-token"Tb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3onAuthErrorTb4b4b4)Tb4b4b4.Te6edf3notTb4b4b4.Te6edf3toHaveBeenCalledTb4b4b4(Tb4b4b4)Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3itTb4b4b4(Ta5d6ff"does not call onAuthError for CSRF 403 responses"Tb4b4b4, Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4{
Tff7b72const Tb4b4b4{ Te6edf3setCsrfToken Tb4b4b4} Tff7b72= Tff7b72await Tff7b72importTb4b4b4(Ta5d6ff"../../meshchatx/src/frontend/js/csrfToken.js"Tb4b4b4)Tb4b4b4;
Te6edf3setCsrfTokenTb4b4b4(Ta5d6ff"stale-token"Tb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3onAuthError Tff7b72= Te6edf3viTb4b4b4.Te6edf3fnTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72const Tb4b4b4{ Te6edf3createApiClient Tb4b4b4} Tff7b72= Tff7b72await Tff7b72importTb4b4b4(Ta5d6ff"../../meshchatx/src/frontend/js/apiClient.js"Tb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3api Tff7b72= Te6edf3createApiClientTb4b4b4(Tb4b4b4{ Te6edf3onAuthError Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3viTb4b4b4.Te6edf3stubGlobalTb4b4b4(Ta5d6ff"fetch"Tb4b4b4, Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4(Tb4b4b4{
Te6edf3okTff7b72: Tff7b72falseTb4b4b4,
Te6edf3statusTff7b72: T79c0ff403Tb4b4b4,
Te6edf3headersTff7b72: Tff7b72new Te6edf3HeadersTb4b4b4(Tb4b4b4{ Ta5d6ff"content-type"Tff7b72: Ta5d6ff"application/json" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Te6edf3textTff7b72: Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tffa657JSONTb4b4b4.Te6edf3stringifyTb4b4b4(Tb4b4b4{ Te6edf3errorTff7b72: Ta5d6ff"Invalid or missing CSRF token" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Tb4b4b4}Tb4b4b4)Tb4b4b4)Tb4b4b4;
T8b949e// Force refresh itself to fail so retry cannot succeed.
Tff7b72await Te6edf3expectTb4b4b4(Te6edf3apiTb4b4b4.Te6edf3postTb4b4b4(Ta5d6ff"/api/v1/destination/deadbeef/request-path"Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3rejectsTb4b4b4.Te6edf3toMatchObjectTb4b4b4(Tb4b4b4{
Te6edf3responseTff7b72: Tb4b4b4{ Te6edf3statusTff7b72: T79c0ff403 Tb4b4b4}Tb4b4b4,
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3onAuthErrorTb4b4b4)Tb4b4b4.Te6edf3notTb4b4b4.Te6edf3toHaveBeenCalledTb4b4b4(Tb4b4b4)Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3itTb4b4b4(Ta5d6ff"still calls onAuthError for real auth 401 responses"Tb4b4b4, Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4{
Tff7b72const Tb4b4b4{ Te6edf3setCsrfToken Tb4b4b4} Tff7b72= Tff7b72await Tff7b72importTb4b4b4(Ta5d6ff"../../meshchatx/src/frontend/js/csrfToken.js"Tb4b4b4)Tb4b4b4;
Te6edf3setCsrfTokenTb4b4b4(Ta5d6ff"good-token"Tb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3onAuthError Tff7b72= Te6edf3viTb4b4b4.Te6edf3fnTb4b4b4(Tb4b4b4)Tb4b4b4;
Tff7b72const Tb4b4b4{ Te6edf3createApiClient Tb4b4b4} Tff7b72= Tff7b72await Tff7b72importTb4b4b4(Ta5d6ff"../../meshchatx/src/frontend/js/apiClient.js"Tb4b4b4)Tb4b4b4;
Tff7b72const Te6edf3api Tff7b72= Te6edf3createApiClientTb4b4b4(Tb4b4b4{ Te6edf3onAuthError Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3viTb4b4b4.Te6edf3stubGlobalTb4b4b4(Ta5d6ff"fetch"Tb4b4b4, Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4(Tb4b4b4{
Te6edf3okTff7b72: Tff7b72falseTb4b4b4,
Te6edf3statusTff7b72: T79c0ff401Tb4b4b4,
Te6edf3headersTff7b72: Tff7b72new Te6edf3HeadersTb4b4b4(Tb4b4b4{ Ta5d6ff"content-type"Tff7b72: Ta5d6ff"application/json" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Te6edf3textTff7b72: Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tffa657JSONTb4b4b4.Te6edf3stringifyTb4b4b4(Tb4b4b4{ Te6edf3errorTff7b72: Ta5d6ff"Authentication required" Tb4b4b4}Tb4b4b4)Tb4b4b4,
Tb4b4b4}Tb4b4b4)Tb4b4b4)Tb4b4b4;
Tff7b72await Te6edf3expectTb4b4b4(Te6edf3apiTb4b4b4.Te6edf3getTb4b4b4(Ta5d6ff"/api/v1/config"Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3rejectsTb4b4b4.Te6edf3toMatchObjectTb4b4b4(Tb4b4b4{
Te6edf3responseTff7b72: Tb4b4b4{ Te6edf3statusTff7b72: T79c0ff401 Tb4b4b4}Tb4b4b4,
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3onAuthErrorTb4b4b4)Tb4b4b4.Te6edf3toHaveBeenCalledTimesTb4b4b4(T79c0ff1Tb4b4b4)Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Te6edf3itTb4b4b4(Ta5d6ff"classifies CSRF rejection bodies without treating other 403s as CSRF"Tb4b4b4, Tff7b72async Tb4b4b4(Tb4b4b4) Tb4b4b4=> Tb4b4b4{
Tff7b72const Tb4b4b4{ Te6edf3isCsrfRejection Tb4b4b4} Tff7b72= Tff7b72await Tff7b72importTb4b4b4(Ta5d6ff"../../meshchatx/src/frontend/js/apiClient.js"Tb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3isCsrfRejectionTb4b4b4(T79c0ff403Tb4b4b4, Tb4b4b4{ Te6edf3errorTff7b72: Ta5d6ff"Invalid or missing CSRF token" Tb4b4b4}Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toBeTb4b4b4(Tff7b72trueTb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3isCsrfRejectionTb4b4b4(T79c0ff403Tb4b4b4, Tb4b4b4{ Te6edf3errorTff7b72: Ta5d6ff"Forbidden: client IP not on allowlist" Tb4b4b4}Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toBeTb4b4b4(Tff7b72falseTb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3isCsrfRejectionTb4b4b4(T79c0ff401Tb4b4b4, Tb4b4b4{ Te6edf3errorTff7b72: Ta5d6ff"Invalid or missing CSRF token" Tb4b4b4}Tb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toBeTb4b4b4(Tff7b72falseTb4b4b4)Tb4b4b4;
Te6edf3expectTb4b4b4(Te6edf3isCsrfRejectionTb4b4b4(T79c0ff403Tb4b4b4, Tff7b72nullTb4b4b4)Tb4b4b4)Tb4b4b4.Te6edf3toBeTb4b4b4(Tff7b72falseTb4b4b4)Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
Tb4b4b4}Tb4b4b4)Tb4b4b4;
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────